// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: jshin

#pragma once

#include <iomanip>
#include <sstream>
#include <string>
#include <glog/logging.h>

#include "coin2/base/time.h"
#include "coin2/exchange/feed.pb.h"

struct Trade {
  int64_t timestamp;
  double price;
  double fill_qty;
  coin2::exchange::feed::TradeSide side;
  bool has_more_trade;

  void Clear() {
    timestamp = 0;
    price = 0;
    fill_qty = 0;
    side = coin2::exchange::feed::TradeSide::TRADE_UNKNOWN_SIDE;
    has_more_trade = false;
  }

  double sign() const {
    if (side == coin2::exchange::feed::TradeSide::TRADE_BUY_SIDE) {
      return 1;
    } else if (side == coin2::exchange::feed::TradeSide::TRADE_SELL_SIDE) {
      return -1;
    } else {
      throw std::runtime_error("uninit trade side");
    }
  }

  double signed_qty() const {
    if (side == coin2::exchange::feed::TradeSide::TRADE_BUY_SIDE) {
      return fill_qty;
    } else if (side == coin2::exchange::feed::TradeSide::TRADE_SELL_SIDE) {
      return -fill_qty;
    } else {
      throw std::runtime_error("uninit trade side");
    }
  }
};

inline bool operator==(const Trade& t1, const Trade& t2) {
  return std::tie(t1.timestamp, t1.price, t1.fill_qty, t1.side) ==
         std::tie(t2.timestamp, t2.price, t2.fill_qty, t2.side);
}

inline bool TryAggregateTrades(Trade* t1, const Trade& t2) {
  if (t1->price == t2.price && t1->side == t2.side) {
    // individual trade may not have timestamp, e.g. huobi fut
    if (t2.timestamp != 0) {
      t1->timestamp = t2.timestamp;
    }
    t1->fill_qty += t2.fill_qty;
    return true;
  }
  return false;
}

inline std::ostream& operator<<(std::ostream& os, const Trade& trade) {
  os << "Trade";
  os << "(price=";
  os << std::setw(15);
  os << trade.price;
  os << ",fill_qty=";
  os << std::setw(15);
  os << trade.fill_qty;
  os << ",side=";
  if (trade.side == coin2::exchange::feed::TRADE_BUY_SIDE) {
    os << "BUY";
  } else if (trade.side == coin2::exchange::feed::TRADE_SELL_SIDE) {
    os << "SELL";
  } else {
    os << "UNKNOWN";
  }
  os << ")";
  return os;
}

struct SlimTrade {
  int64_t packet_timestamp;
  int64_t timestamp;
  double pq_sum;
  double q_sum;
  coin2::exchange::feed::TradeSide side;
  int64_t packet_timestamp_to_publish;
  int64_t timestamp_to_publish;
  double price_to_publish;
  double qty_to_publish;
  coin2::exchange::feed::TradeSide side_to_publish;
  bool ready_to_publish;
  int64_t published_timestamp;

  SlimTrade() {
    ClearCore();
    ClearToPublish();
  }

  void ClearCore() {
    packet_timestamp = 0;
    timestamp = 0;
    pq_sum = 0;
    q_sum = 0;
    side = coin2::exchange::feed::TradeSide::TRADE_UNKNOWN_SIDE;
  }

  void ClearToPublish() {
    packet_timestamp_to_publish = 0;
    timestamp_to_publish = 0;
    price_to_publish = 0;
    qty_to_publish = 0;
    side_to_publish = coin2::exchange::feed::TradeSide::TRADE_UNKNOWN_SIDE;
    ready_to_publish = false;
  }

  bool TryAggregateSlimTrade(
      int64_t packet_timestamp,
      double price,
      double qty,
      coin2::exchange::feed::TradeSide side,
      int64_t timestamp,
      const double sampling_frequency_bps) {
    // sampling_frequency_bps is unused for now,
    // borrow book move 2bps condition instead.
    (void)sampling_frequency_bps;
    // If it's ready to publish, to aggregate more might lead to trade drop.
    CHECK(!ready_to_publish);
    if (this->timestamp == 0 || this->side == side) {
      this->packet_timestamp = packet_timestamp;
      this->timestamp = timestamp;
      this->pq_sum += price * qty;
      this->q_sum += qty;
      this->side = side;
      return true;
    } else {
      this->packet_timestamp_to_publish = this->packet_timestamp;
      this->timestamp_to_publish = this->timestamp;
      this->price_to_publish = this->pq_sum / this->q_sum;
      this->qty_to_publish = this->q_sum;
      this->side_to_publish = this->side;
      ready_to_publish = true;
      this->packet_timestamp = packet_timestamp;
      this->timestamp = timestamp;
      this->pq_sum = price * qty;
      this->q_sum = qty;
      this->side = side;
      return false;
    }
  }

  bool PopulateTradeAndClear(Trade* trade) {
    if (ready_to_publish) {
      published_timestamp = this->packet_timestamp_to_publish;
      trade->timestamp = this->timestamp_to_publish;
      trade->price = this->price_to_publish;
      trade->fill_qty = this->qty_to_publish;
      trade->side = this->side_to_publish;
      ClearToPublish();
      return true;
    } else if (timestamp != 0) {
      this->packet_timestamp_to_publish = this->packet_timestamp;
      this->timestamp_to_publish = this->timestamp;
      this->price_to_publish = this->pq_sum / this->q_sum;
      this->qty_to_publish = this->q_sum;
      this->side_to_publish = this->side;
      ready_to_publish = true;

      published_timestamp = this->packet_timestamp_to_publish;
      trade->timestamp = this->timestamp_to_publish;
      trade->price = this->price_to_publish;
      trade->fill_qty = this->qty_to_publish;
      trade->side = this->side_to_publish;
      ClearToPublish();
      ClearCore();
      return true;
    } else {
      // Really nothing to populate.
      return false;
    }
  }

  inline int64_t LastPublishedTimestamp() const {
    return this->published_timestamp;
  }
};
